package com.vlyman.weixin.utils;

import com.alibaba.fastjson.JSONObject;
import com.vlyman.weixin.consts.WxUriConsts;
import com.vlyman.weixin.entity.WxMicroPayVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.net.ssl.SSLContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.*;
import java.util.Map.Entry;

/**
 * 类描述：
 *   微信接口工具类
 *  @author chenjs
 *  @date 2018/11/1 13:50
 **/
@Slf4j
public class WxCommonUtil {

	public static final String WX_RTN_ERRCODE = "errcode";

	/**
	 * 方法描述：<br>
	 * 	获取32位UUID
	 * @author chenjs
	 * @return String
	 * @date 2017年7月3日
	 */
	public static String getUUID32(){
		return UUID.randomUUID().toString().replaceAll("-", "");
	}

	/**
	 * 方法描述：
	 *   对字符串数据进行脱敏处理
	 *  @author chenjs
	 *  @param head 开头保留显式数量
	 *  @param foot	结尾保留显式数量
	 *  @param source	脱敏字段
	 *  @return String
	 *  @date 2018/11/1 15:42
	 */
	public static String secureDataStr(int head, int foot, String source) {
		if (source.length() <= (head+foot)) {
			//若显式数量大于字符长度，则直接返回原有字符串
			return source;
		}
		String headStr = source.substring(0, head);
		String footStr = source.substring(source.length()-foot, source.length());
		StringBuffer res = new StringBuffer(headStr);
		for (int i = 0; i < (source.length()-head-foot); i++) {
			res.append("*");
		}
		res.append(footStr);
		return res.toString();
	}
	
	/**
	 * 方法描述：<br>
	 * 	字符串的字典序排序后进行sha1加密
	 * @author chenjs
	 * @param paramList
	 * @return String
	 * @date 2017年7月3日
	 */
	public static String sha1Signature(List<String> paramList){
		Collections.sort(paramList);
		StringBuffer preEncode = new StringBuffer();
		Iterator<String> itr = paramList.iterator();
		while (itr.hasNext()) {
			String target = (String) itr.next();
			preEncode.append(target);
		}
		log.info("SHA1加密前：".concat(preEncode.toString()));
		log.info("SHA1加密后：".concat(DigestUtils.sha1Hex(preEncode.toString())));
		return DigestUtils.sha1Hex(preEncode.toString());
	}
	
	/**
	 * 方法描述：<br>
	 * 	微信支付签名（MD5）
	 * @author chenjs
	 * @param packageParams
	 * @param payKey
	 * @return String
	 * @date 2017年7月16日
	 */
	public static String md5Signature(SortedMap<String, String> packageParams, String payKey){
		StringBuffer sb = new StringBuffer();
		Set<Entry<String, String>> set = packageParams.entrySet();
		Iterator<Entry<String, String>> itr = set.iterator();
		while (itr.hasNext()) {
			Entry<String, String> entry = itr.next();
			String key = entry.getKey();
			String value = entry.getValue();
			if (StringUtils.isNotBlank(value)) {
				sb.append(key);
				sb.append("=");
				sb.append(value);
				sb.append("&");
			}
		}
		sb.append("key=");
		sb.append(payKey);
		log.info("MD5加密前：".concat(sb.toString()));
		String sign = DigestUtils.md5Hex(sb.toString()).toUpperCase();
		log.info("MD5加密后：".concat(sign));
		return sign;
	}

	/**
	 * 方法描述：<br>
	 * 	构造微信支付xml请求报文
	 * @author chenjs
	 * @param packageParams
	 * @return String
	 * @date 2017年7月16日
	 */
	public static String buildPayXml(SortedMap<String, String> packageParams){
		Document document = DocumentHelper.createDocument();
		Element root = document.addElement("xml");
		Set<Entry<String, String>> set = packageParams.entrySet();
		Iterator<Entry<String, String>> itr = set.iterator();
		while (itr.hasNext()) {
			Entry<String, String> entry = itr.next();
			String key = entry.getKey();
			String value = entry.getValue();
			if (StringUtils.isNotBlank(value)) {
				if ("detail".equals(key)) {
					root.addElement(key).addCDATA(value);
				} else {
					root.addElement(key).setText(value);
				}
			}
		}
		return document.asXML();
	}
	
	/**
	 * 方法描述：<br>
	 * 	将xml报文转化为map对象
	 * @author chenjs
	 * @param xml
	 * @return Map<String, Object>
	 * @date 2017年7月16日
	 */
	public static Map<String, Object> transferXmlToMap(String xml){
		try {
			Map<String, Object> data = new HashMap<>(32);
			//修复微信支付XXE漏洞
			DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			String feature = null;
			try {
				feature = "http://apache.org/xml/features/disallow-doctype-decl";
				documentBuilderFactory.setFeature(feature, true);
				feature = "http://xml.org/sax/features/external-general-entities";
				documentBuilderFactory.setFeature(feature, false);
				feature = "http://xml.org/sax/features/external-parameter-entities";
				documentBuilderFactory.setFeature(feature, false);
				feature = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
				documentBuilderFactory.setFeature(feature, false);
				documentBuilderFactory.setXIncludeAware(false);
				documentBuilderFactory.setExpandEntityReferences(false);
			} catch (ParserConfigurationException e) {
				log.error("ParserConfigurationException was thrown. The feature '" +
						feature + "' is probably not supported by your XML processor.");
			}
			DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
			InputStream stream = new ByteArrayInputStream(xml.getBytes("UTF-8"));
			org.w3c.dom.Document doc = documentBuilder.parse(stream);
			doc.getDocumentElement().normalize();
			NodeList nodeList = doc.getDocumentElement().getChildNodes();
			for (int idx = 0; idx < nodeList.getLength(); ++idx) {
				Node node = nodeList.item(idx);
				if (node.getNodeType() == Node.ELEMENT_NODE) {
					org.w3c.dom.Element element = (org.w3c.dom.Element) node;
					data.put(element.getNodeName(), element.getTextContent());
				}
			}
			try {
				stream.close();
			} catch (Exception ex) {
				// do nothing
			}
			return data;
		} catch (Exception ex) {
			log.error("xml报文转换失败！", ex);
			return null;
		}
	}

	/**
	 * 方法描述：<br>
	 * 	微信卡券核销Code
	 * @author chenjs
	 * @param code
	 * @date 2017年7月18日
	 */
	public static boolean consumeWxCard(String accessToken, String code){
		String reqUrl = WxUriConsts.WX_CARD_CONSUME.concat(accessToken);
		Map<String, String> params = new HashMap<>(4);
		params.put("code", code);
		try {
			String respStr = HttpUtil.post(reqUrl, JSONObject.toJSONString(params));
			JSONObject resp = JSONObject.parseObject(respStr);
			if (resp.getIntValue(WX_RTN_ERRCODE) != 0) {
				log.error("卡券核销失败！报文：".concat(respStr));
				return false;
			}
			return true;
		} catch (Exception e) {
			log.error("卡券核销失败！", e);
			return false;
		}
	}
	
	/**
	 * 方法描述：<br>
	 * 	微信卡券Code解码
	 * @author chenjs
	 * @param encryptCode
	 * @return String
	 * @date 2017年7月18日
	 */
	public static String decryptCode(String accessToken, String encryptCode){
		String reqUrl = WxUriConsts.WX_CARD_DECRYPT.concat(accessToken);
		Map<String, String> params = new HashMap<>(4);
		params.put("encrypt_code", encryptCode);
		try {
			String respStr = HttpUtil.post(reqUrl, JSONObject.toJSONString(params));
			JSONObject resp = JSONObject.parseObject(respStr);
			if (resp.getIntValue(WX_RTN_ERRCODE) == 0) {
				return resp.getString("code");
			} else {
				return "";
			}
		} catch (Exception e) {
			log.error("微信卡券Code解码失败！", e);
			return "";
		}
	}
	
	/**
	 * 方法描述：<br>
	 * 	构造微信支付通知返回报文
	 * @author chenjs
	 * @param isSuccess
	 * @param msg
	 * @return String
	 * @date 2017年7月20日
	 */
	public static String buidNotifyRtnXml(boolean isSuccess, String msg){
		Document document = DocumentHelper.createDocument();
		Element root = document.addElement("xml");
		if (isSuccess) {
			root.addElement("return_code").addCDATA("SUCCESS");
			root.addElement("return_msg").addCDATA("OK");
		} else {
			root.addElement("return_code").addCDATA("FAIL");
			root.addElement("return_msg").addCDATA(msg);
		}
		return root.asXML();
	}
	
	/**
	 * 方法描述：<br>
	 * 	查询微信卡券Code
	 * @author: chenjs
	 * @param code
	 * @return
	 * @date: 2017年8月25日
	 */
	public static JSONObject checkCode(String accessToken, String code){
		String reqUrl = WxUriConsts.WX_CARD_GET.concat(accessToken);
		Map<String, Object> params = new HashMap<>(4);
		params.put("code", code);
		params.put("check_consume", false);
		try {
			String respStr = HttpUtil.post(reqUrl, JSONObject.toJSONString(params));
			JSONObject resp = JSONObject.parseObject(respStr);
			if (resp.getIntValue(WX_RTN_ERRCODE) != 0) {
				log.error("查询卡券code失败！返回报文：".concat(respStr));
			}
			return resp;
		} catch (Exception e) {
			log.error("查询卡券code失败！", e);
			return null;
		}
	}
	
	/**
	 * 方法描述：<br>
	 * 	获取用户基本信息
	 * @author: chenjs
	 * @param accessToken
	 * @param openId
	 * @return
	 * @date: 2017年9月13日
	 */
	public static JSONObject getWxuserInfo(String accessToken, String openId){
		String reqUrl = WxUriConsts.WX_USER_INFO
				.concat("?access_token=").concat(accessToken)
				.concat("&openid=").concat(openId)
				.concat("&lang=zh_CN");

		try {
			String respStr = HttpUtil.get(reqUrl);
			log.info("获取微信用户基本信息，接口返回：".concat(respStr));
			return JSONObject.parseObject(respStr);
		} catch (Exception e) {
			log.error("获取微信用户基本信息失败！", e);
			return null;
		}
	}
	
	/**
	 * 方法描述：<br>
	 * 	获取access_token
	 * <li>access_token是公众号的全局唯一接口调用凭据，公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。
	 * access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时，需定时刷新，重复获取将导致上次获取的access_token失效。</li>
	 * @author: chenjs
	 * @param appId
	 * @param appSecret
	 * @return
	 * @date: 2017年12月17日
	 */
	public static JSONObject getAccessToken(String appId, String appSecret){
		String reqUrl = WxUriConsts.WX_ACCESS_TOKEN
				.concat("&appid=").concat(appId)
				.concat("&secret=").concat(appSecret);
		try {
			String respStr = HttpUtil.get(reqUrl);
			log.info("获取access_token，接口返回：".concat(respStr));
			return JSONObject.parseObject(respStr);
		} catch (Exception e) {
			log.error("获取access_token失败！", e);
			return null;
		}
	}

	/**
	 * 方法描述：<br>
	 * 	获取网页授权access_token，通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token不同
	 * @author chenjs
	 * @param code
	 * @return OperationResult
	 * @date 2016年11月15日
	 */
	public static JSONObject getWxJsAccessToken(String appId, String appSecret, String code){
		try {
			String respStr = HttpUtil.get(WxUriConsts.WX_OAUTH_ACCESS_TOKEN
					.concat("?appid=").concat(appId)
					.concat("&secret=").concat(appSecret)
					.concat("&code=").concat(code)
					.concat("&grant_type=authorization_code"));
			return JSONObject.parseObject(respStr);
		} catch (Exception e) {
			log.error("获取网页授权access_token出错！", e);
			return null;
		}
	}
	
	/**
	 * 方法描述：<br>
	 * 	获取jsapi_ticket
	 * <li>jsapi_ticket是公众号用于调用微信JS接口的临时票据。正常情况下，jsapi_ticket的有效期为7200秒，通过access_token来获取</li>
	 * @author: chenjs
	 * @param accessToken
	 * @return
	 * @date: 2017年12月17日
	 */
	public static JSONObject getjsapiTicket(String accessToken){
		String reqUrl = WxUriConsts.WX_JS_TICKET
				.concat("&access_token=").concat(accessToken);
		try {
			String respStr = HttpUtil.get(reqUrl);
			log.info("获取jsapi_ticket，接口返回：".concat(respStr));
			return JSONObject.parseObject(respStr);
		} catch (Exception e) {
			log.error("获取jsapi_ticket失败！", e);
			return null;
		}
	}
	
	/**
	 * 方法描述：
	 * 	获取wxCardTicket
	 * <li>卡券 api_ticket 是用于调用卡券相关接口的临时票据，有效期为 7200 秒，通过 access_token 来获取</li>
	 * @author: chenjs
	 * @param accessToken
	 * @return
	 * @date: 2017年12月17日
	 */
	public static JSONObject getwxCardTicket(String accessToken){
		String reqUrl = WxUriConsts.WX_CARD_TICKET
				.concat(accessToken);
		try {
			String respStr = HttpUtil.get(reqUrl);
			log.info("获取wxCardTicket，接口返回：".concat(respStr));
			return JSONObject.parseObject(respStr);
		} catch (Exception e) {
			log.error("获取wxCardTicket失败！", e);
			return null;
		}
	}

	/**
	 * 方法描述：
	 *	生成带参数的二维码
	 *	<li>1、临时二维码，是有过期时间的，最长可以设置为在二维码生成后的30天（即2592000秒）后过期，但能够生成较多数量。临时二维码主要用于帐号绑定等不要求二维码永久保存的业务场景</li>
	 *	<li>2、永久二维码，是无过期时间的，但数量较少（目前为最多10万个）。永久二维码主要用于适用于帐号绑定、用户来源统计等场景。</li>
	 * @author chenjs
	 * @param accessToken	接口票据
	 * @param isLimit		是否为永久带参数二维码
	 * @param scene			场景值
	 * @param expireSeconds	过期时间（秒）
	 * @return JSONObject
	 * @date 2018/6/28
	 */
	public static JSONObject getQrCodeTicket(String accessToken, boolean isLimit, String scene, int expireSeconds) {
		String finalParams;
		if (isLimit) {
			//生成永久带参数二维码
			String limitQrCode = "{\"action_name\":\"QR_LIMIT_STR_SCENE\",\"action_info\":{\"scene\": {\"scene_str\":\"SCENE_STR\"}}}";
			finalParams = limitQrCode.replace("SCENE_STR", scene);
		} else {
			//生成临时带参数二维码
			String tmpQrCode = "{\"expire_seconds\":EXPIRE_SECONDS,\"action_name\":\"QR_STR_SCENE\",\"action_info\": {\"scene\":{\"scene_str\":\"SCENE_STR\"}}}";
			finalParams = tmpQrCode.replace("EXPIRE_SECONDS", expireSeconds+"").replace("SCENE_STR", scene);
		}

		String reqUrl = WxUriConsts.WX_QRCODE_CREATE.concat("?access_token=").concat(accessToken);
		try {
			String respStr = HttpUtil.post(reqUrl, finalParams);
			return JSONObject.parseObject(respStr);
		} catch (Exception e) {
			log.error("生成带参数的二维码失败！", e);
			return null;
		}
	}

	/**
	 * 方法描述：
	 * 	微信发放普通红包
	 * @author chenjs
	 * @param appId				商户AppId
	 * @param mchId				微信支付商户id
	 * @param payKey			微信支付密钥
	 * @param orderNum			订单号
	 * @param receiverOpenId	接受人openId
	 * @param redPacketMoney	红包金额(单位：分)
	 * @return String
	 * @date 2018/7/17
	 */
	public static String sendWxRedpacket(String appId, String mchId, String payKey, String certPath,
										 String orderNum, String receiverOpenId, int redPacketMoney, String clientIp,
										 String sendName, String wishing, String actName, String remark) {
		try {
			String nonceStr = WxCommonUtil.getUUID32();
			int totalNum = 1;
			//抽奖
			String sceneId="PRODUCT_2";
			SortedMap<String, String> packageParams = new TreeMap<>();
			packageParams.put("nonceStr", nonceStr);
			packageParams.put("mchBillno", orderNum);
			packageParams.put("mchId", mchId);
			packageParams.put("wxappid", appId);
			packageParams.put("sendName", sendName);
			packageParams.put("reOpenid", receiverOpenId);
			//付款金额，单位分，抽奖最少0.3元
			packageParams.put("totalAmount", String.valueOf(redPacketMoney));
			packageParams.put("totalNum", String.valueOf(totalNum));
			packageParams.put("wishing", wishing);
			//需要在微信支付商户平台-红包产品中添加该IP白名单
			packageParams.put("client_ip", clientIp);
			packageParams.put("actName", actName);
			packageParams.put("remark", remark);
			packageParams.put("sceneId", sceneId);
			String response = WxCommonUtil.certPostRequestCommon(WxUriConsts.WX_RED_PACK_SEND,
					packageParams, payKey, certPath, mchId);
			Map<String, Object> resMap = WxCommonUtil.transferXmlToMap(response);
			String resultCode = (String) resMap.get("result_code");
			log.info("返回：".concat(response));
			return resultCode;
		} catch (Exception e) {
			log.error("微信发放普通红包失败！", e);
			return "";
		}
	}

	/**
	 * 方法描述：
	 * 	需要用支付证书的通用post调用方法
	 * @author chenjs
	 * @param url	请求地址
	 * @param packageParams	封装参数
	 * @param payKey	商户平台设置的API密钥
	 * @return String
	 * @date 2018/3/20
	 */
	public static String certPostRequestCommon(String url, SortedMap<String, String> packageParams, String payKey, String wxCertPath, String wxMchId) {
		try {
			KeyStore keyStore  = KeyStore.getInstance("PKCS12");
			FileInputStream inputstream = new FileInputStream(new File(wxCertPath));
			keyStore.load(inputstream, wxMchId.toCharArray());
			SSLContext sslcontext = SSLContexts.custom()
					.loadKeyMaterial(keyStore, wxMchId.toCharArray())
					.build();
			SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
					sslcontext,
					new String[] { "TLSv1" },
					null,
					SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
			CloseableHttpClient httpclient = HttpClients.custom()
					.setSSLSocketFactory(sslsf)
					.build();
			HttpPost httpPost = new HttpPost(url);
			String signature = WxCommonUtil.md5Signature(packageParams, payKey);
			packageParams.put("sign", signature);
			String xml = WxCommonUtil.buildPayXml(packageParams);
			log.info("=======>带证书的post请求，请求报文："+xml);
			StringEntity entity = new StringEntity(xml, "UTF-8");
			entity.setContentEncoding("UTF-8");
			entity.setContentType("application/json");
			httpPost.setEntity(entity);

			CloseableHttpResponse response = httpclient.execute(httpPost);
			HttpEntity responseEntity = response.getEntity();
			String responseStr = "";
			if (responseEntity != null) {
				responseStr = IOUtils.toString(responseEntity.getContent(), "UTF-8");
			}
			EntityUtils.consume(entity);
			log.info("=======>带证书的post请求，返回报文："+responseStr);
			return responseStr;
		} catch (Exception e) {
			log.error("============>方法：certPostRequestCommon，请求出错", e);
		}
		return "";
	}

	/**
	 * 方法描述：
	 *	 提交付款码支付
	 *  @author chenjs
	 *  @param payVo
	 *  @return JSONObject
	 *  @date 2019/7/17 23:34
	 */
	public static JSONObject wxMicorPay(WxMicroPayVo payVo) {
		SortedMap<String, String> packageParams = new TreeMap<>();
		packageParams.put("appid", payVo.getAppId());
		packageParams.put("mch_id", payVo.getMchId());
		packageParams.put("nonce_str", WxCommonUtil.getUUID32());
		packageParams.put("body", "付款码支付");
		packageParams.put("out_trade_no", payVo.getOutTradeNo());
		packageParams.put("total_fee", String.valueOf(payVo.getTotalFee()));
		packageParams.put("spbill_create_ip", payVo.getSpbillCreateIp());
		packageParams.put("auth_code", payVo.getAuthCode());
		String signature = WxCommonUtil.md5Signature(packageParams, payVo.getPayKey());
		packageParams.put("sign", signature);
		String xml = WxCommonUtil.buildPayXml(packageParams);
		try {
			String resp = HttpUtil.post(WxUriConsts.WX_PAY_MICRO, xml);
			Map<String, Object> resMap = WxCommonUtil.transferXmlToMap(resp);
			return JSONObject.parseObject(JSONObject.toJSONString(resMap));
		} catch (Exception e) {
			log.error("付款码支付失败！", e);
			return null;
		}
	}
}
